/*
 * PageSetupPanel.java 27 aout 07
 *
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.PrinterJob;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.security.AccessControlException;

import javax.swing.ActionMap;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.model.HomePrint;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.swing.NullableSpinner.NullableSpinnerNumberModel;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.PageSetupController;
import com.eteks.sweethome3d.viewcontroller.View;

/**
 * Home page setup editing panel.
 * @author Emmanuel Puybaret
 */
public class PageSetupPanel extends JPanel implements DialogView
{
	private static final int DEFAULT_SCALE = 100;
	
	private final PageSetupController controller;
	private PageFormat pageFormat;
	private JButton pageFormatButton;
	private JCheckBox furniturePrintedCheckBox;
	private JCheckBox planPrintedCheckBox;
	private JRadioButton bestFitPlanScaleRadioButton;
	private JRadioButton userPlanScaleRadioButton;
	private JSpinner userPlanScaleSpinner;
	private JCheckBox view3DPrintedCheckBox;
	private JLabel headerFormatLabel;
	private JTextField headerFormatTextField;
	private JLabel footerFormatLabel;
	private JTextField footerFormatTextField;
	private JLabel variablesLabel;
	private JToolBar variableButtonsToolBar;
	private String dialogTitle;
	
	/**
	 * Creates a panel that displays page setup.
	 * @param controller the controller of this panel
	 */
	public PageSetupPanel(UserPreferences preferences, PageSetupController controller)
	{
		super(new GridBagLayout());
		this.controller = controller;
		createActions(preferences);
		createComponents(preferences, controller);
		setMnemonics(preferences);
		layoutComponents();
	}
	
	/**
	 * Creates actions for variables.
	 */
	private void createActions(UserPreferences preferences)
	{
		ActionMap actions = getActionMap();
		for (final HomePrintableComponent.Variable variable : HomePrintableComponent.Variable.values())
		{
			actions.put(variable, new ResourceAction(preferences, PageSetupPanel.class, "INSERT_" + variable.name())
			{
				@Override
				public void actionPerformed(ActionEvent ev)
				{
					insertVariable(variable.getUserCode());
				}
			});
		}
	}
	
	/**
	 * Inserts a code in the text field that has the focus and selects it.
	 */
	private void insertVariable(String userCode)
	{
		Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
		if (focusOwner instanceof JTextField)
		{
			JTextField textField = (JTextField) focusOwner;
			textField.replaceSelection(userCode);
			int lastCharacter = textField.getCaretPosition();
			int firstCharacter = lastCharacter - userCode.length();
			textField.select(firstCharacter, lastCharacter);
		}
	}
	
	/**
	 * Creates and initializes components.
	 */
	private void createComponents(UserPreferences preferences, final PageSetupController controller)
	{
		this.pageFormatButton = new JButton(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "pageFormatButton.text"));
		this.furniturePrintedCheckBox = new JCheckBox(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "furniturePrintedCheckBox.text"));
		this.planPrintedCheckBox = new JCheckBox(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "planPrintedCheckBox.text"));
		
		// Create scale radio buttons and user's scale spinner
		this.bestFitPlanScaleRadioButton = new JRadioButton(SwingTools.getLocalizedLabelText(preferences,
				PageSetupPanel.class, "bestFitPlanScaleRadioButton.text"));
		this.userPlanScaleRadioButton = new JRadioButton(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "userPlanScaleRadioButton.text"));
		ButtonGroup scaleButtonsGroup = new ButtonGroup();
		scaleButtonsGroup.add(this.bestFitPlanScaleRadioButton);
		scaleButtonsGroup.add(this.userPlanScaleRadioButton);
		final NullableSpinner.NullableSpinnerNumberModel userPlanScaleSpinnerModel = new NullableSpinner.NullableSpinnerNumberModel(
				10, 1, 10000, 10);
		this.userPlanScaleSpinner = new AutoCommitSpinner(userPlanScaleSpinnerModel);
		
		this.view3DPrintedCheckBox = new JCheckBox(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "view3DPrintedCheckBox.text"));
		
		this.headerFormatLabel = new JLabel(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "headerFormatLabel.text"));
		this.headerFormatTextField = new JTextField(20);
		if (!OperatingSystem.isMacOSXLeopardOrSuperior())
		{
			SwingTools.addAutoSelectionOnFocusGain(this.headerFormatTextField);
		}
		
		this.footerFormatLabel = new JLabel(
				SwingTools.getLocalizedLabelText(preferences, PageSetupPanel.class, "footerFormatLabel.text"));
		this.footerFormatTextField = new JTextField(20);
		if (!OperatingSystem.isMacOSXLeopardOrSuperior())
		{
			SwingTools.addAutoSelectionOnFocusGain(this.footerFormatTextField);
		}
		
		// Create variables buttons tool bar
		this.variablesLabel = new JLabel(preferences.getLocalizedString(PageSetupPanel.class, "variablesLabel.text"));
		this.variableButtonsToolBar = new JToolBar();
		this.variableButtonsToolBar.setFloatable(false);
		ActionMap actions = getActionMap();
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.PAGE_NUMBER));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.PAGE_COUNT));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.PLAN_SCALE));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.DATE));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.TIME));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.HOME_PRESENTATION_NAME));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.HOME_NAME));
		this.variableButtonsToolBar.add(actions.get(HomePrintableComponent.Variable.LEVEL_NAME));
		for (int i = 0, n = this.variableButtonsToolBar.getComponentCount(); i < n; i++)
		{
			JComponent component = (JComponent) this.variableButtonsToolBar.getComponentAtIndex(i);
			// Remove focusable property on buttons
			component.setFocusable(false);
		}
		
		updateComponents(controller.getPrint());
		
		final PropertyChangeListener printChangeListener = new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				updateComponents(controller.getPrint());
			}
		};
		this.pageFormatButton.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent ev)
			{
				// Show the page setup dialog
				PrinterJob printerJob = PrinterJob.getPrinterJob();
				pageFormat = printerJob.pageDialog(pageFormat);
				updateController(controller);
			}
		});
		ItemListener itemListener = new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				updateController(controller);
			}
		};
		this.furniturePrintedCheckBox.addItemListener(itemListener);
		this.planPrintedCheckBox.addItemListener(itemListener);
		userPlanScaleSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				updateController(controller);
			}
		});
		this.bestFitPlanScaleRadioButton.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent ev)
			{
				updateController(controller);
			}
		});
		this.userPlanScaleRadioButton.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent ev)
			{
				if (userPlanScaleRadioButton.isSelected() && userPlanScaleSpinnerModel.getValue() == null)
				{
					userPlanScaleSpinnerModel.setValue(DEFAULT_SCALE);
				}
				else
				{
					updateController(controller);
				}
			}
		});
		this.view3DPrintedCheckBox.addItemListener(itemListener);
		controller.addPropertyChangeListener(PageSetupController.Property.PRINT, printChangeListener);
		
		DocumentListener documentListener = new DocumentListener()
		{
			public void changedUpdate(DocumentEvent ev)
			{
				controller.removePropertyChangeListener(PageSetupController.Property.PRINT, printChangeListener);
				updateController(controller);
				controller.addPropertyChangeListener(PageSetupController.Property.PRINT, printChangeListener);
			}
			
			public void insertUpdate(DocumentEvent ev)
			{
				changedUpdate(ev);
			}
			
			public void removeUpdate(DocumentEvent ev)
			{
				changedUpdate(ev);
			}
		};
		this.headerFormatTextField.getDocument().addDocumentListener(documentListener);
		FocusListener textFieldFocusListener = new FocusListener()
		{
			public void focusGained(FocusEvent ev)
			{
				ActionMap actionMap = getActionMap();
				for (HomePrintableComponent.Variable field : HomePrintableComponent.Variable.values())
				{
					actionMap.get(field).setEnabled(true);
				}
			}
			
			public void focusLost(FocusEvent ev)
			{
				ActionMap actionMap = getActionMap();
				for (HomePrintableComponent.Variable field : HomePrintableComponent.Variable.values())
				{
					actionMap.get(field).setEnabled(false);
				}
			}
		};
		this.headerFormatTextField.addFocusListener(textFieldFocusListener);
		this.footerFormatTextField.getDocument().addDocumentListener(documentListener);
		this.footerFormatTextField.addFocusListener(textFieldFocusListener);
		
		this.dialogTitle = preferences.getLocalizedString(PageSetupPanel.class, "pageSetup.title");
	}
	
	/**
	 * Updates components from <code>homePrint</code> attributes.
	 */
	private void updateComponents(HomePrint homePrint)
	{
		this.pageFormat = HomePrintableComponent.getPageFormat(homePrint);
		boolean no3D;
		try
		{
			no3D = Boolean.getBoolean("com.eteks.sweethome3d.no3D");
		}
		catch (AccessControlException ex)
		{
			// If com.eteks.sweethome3d.no3D property can't be read, 
			// security manager won't allow to access to Java 3D DLLs required to print 3D images too
			no3D = true;
		}
		// Check if off screen image is supported 
		boolean offscreenCanvas3DSupported;
		if (no3D)
		{
			offscreenCanvas3DSupported = false;
		}
		else
		{
			offscreenCanvas3DSupported = Component3DManager.getInstance().isOffScreenImageSupported();
		}
		final NullableSpinnerNumberModel userPlanScaleSpinnerModel = (NullableSpinner.NullableSpinnerNumberModel) this.userPlanScaleSpinner
				.getModel();
		if (homePrint != null)
		{
			this.furniturePrintedCheckBox.setSelected(homePrint.isFurniturePrinted());
			this.planPrintedCheckBox.setSelected(homePrint.isPlanPrinted());
			this.bestFitPlanScaleRadioButton.setEnabled(homePrint.isPlanPrinted());
			this.userPlanScaleRadioButton.setEnabled(homePrint.isPlanPrinted());
			if (homePrint.getPlanScale() == null)
			{
				this.bestFitPlanScaleRadioButton.setSelected(true);
			}
			else
			{
				this.userPlanScaleRadioButton.setSelected(true);
			}
			this.userPlanScaleSpinner.setEnabled(homePrint.isPlanPrinted() && homePrint.getPlanScale() != null);
			this.view3DPrintedCheckBox.setSelected(homePrint.isView3DPrinted() && offscreenCanvas3DSupported);
			userPlanScaleSpinnerModel.setNullable(homePrint.getPlanScale() == null);
			userPlanScaleSpinnerModel.setValue(
					homePrint.getPlanScale() != null ? new Integer(Math.round(1 / homePrint.getPlanScale())) : null);
			String headerFormat = homePrint.getHeaderFormat();
			this.headerFormatTextField.setText(headerFormat != null ? headerFormat : "");
			String footerFormat = homePrint.getFooterFormat();
			this.footerFormatTextField.setText(footerFormat != null ? footerFormat : "");
		}
		else
		{
			this.furniturePrintedCheckBox.setSelected(true);
			this.planPrintedCheckBox.setSelected(true);
			this.bestFitPlanScaleRadioButton.setEnabled(true);
			this.bestFitPlanScaleRadioButton.setSelected(true);
			this.userPlanScaleRadioButton.setEnabled(true);
			this.userPlanScaleSpinner.setEnabled(false);
			this.view3DPrintedCheckBox.setSelected(offscreenCanvas3DSupported);
			userPlanScaleSpinnerModel.setNullable(true);
			userPlanScaleSpinnerModel.setValue(null);
			this.headerFormatTextField.setText("");
			this.footerFormatTextField.setText("");
		}
		this.view3DPrintedCheckBox.setEnabled(offscreenCanvas3DSupported);
		this.view3DPrintedCheckBox.setVisible(!no3D);
	}
	
	/**
	 * Updates controller print attributes.
	 */
	public void updateController(PageSetupController controller)
	{
		// Return an HomePrint instance matching returnedPageFormat
		HomePrint.PaperOrientation paperOrientation;
		switch (this.pageFormat.getOrientation())
		{
			case PageFormat.LANDSCAPE:
				paperOrientation = HomePrint.PaperOrientation.LANDSCAPE;
				break;
			case PageFormat.REVERSE_LANDSCAPE:
				paperOrientation = HomePrint.PaperOrientation.REVERSE_LANDSCAPE;
				break;
			default:
				paperOrientation = HomePrint.PaperOrientation.PORTRAIT;
				break;
		}
		Paper paper = this.pageFormat.getPaper();
		HomePrint homePrint = new HomePrint(paperOrientation, (float) paper.getWidth(), (float) paper.getHeight(),
				(float) paper.getImageableY(), (float) paper.getImageableX(),
				(float) (paper.getHeight() - paper.getImageableHeight() - paper.getImageableY()),
				(float) (paper.getWidth() - paper.getImageableWidth() - paper.getImageableX()),
				this.furniturePrintedCheckBox.isSelected(), this.planPrintedCheckBox.isSelected(),
				this.view3DPrintedCheckBox.isSelected(),
				this.userPlanScaleRadioButton.isSelected() && this.userPlanScaleSpinner.getValue() != null
						? 1f / ((Number) this.userPlanScaleSpinner.getValue()).intValue() : null,
				this.headerFormatTextField.getText().trim(), this.footerFormatTextField.getText().trim());
		controller.setPrint(homePrint);
	}
	
	/**
	 * Sets components mnemonics and label / component associations.
	 */
	private void setMnemonics(UserPreferences preferences)
	{
		if (!OperatingSystem.isMacOSX())
		{
			this.pageFormatButton.setMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PageSetupPanel.class, "pageFormatButton.mnemonic"))
					.getKeyCode());
			this.furniturePrintedCheckBox.setMnemonic(KeyStroke
					.getKeyStroke(
							preferences.getLocalizedString(PageSetupPanel.class, "furniturePrintedCheckBox.mnemonic"))
					.getKeyCode());
			this.planPrintedCheckBox.setMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PageSetupPanel.class, "planPrintedCheckBox.mnemonic"))
					.getKeyCode());
			this.view3DPrintedCheckBox.setMnemonic(KeyStroke
					.getKeyStroke(
							preferences.getLocalizedString(PageSetupPanel.class, "view3DPrintedCheckBox.mnemonic"))
					.getKeyCode());
			this.bestFitPlanScaleRadioButton.setMnemonic(KeyStroke.getKeyStroke(
					preferences.getLocalizedString(PageSetupPanel.class, "bestFitPlanScaleRadioButton.mnemonic"))
					.getKeyCode());
			this.userPlanScaleRadioButton.setMnemonic(KeyStroke
					.getKeyStroke(
							preferences.getLocalizedString(PageSetupPanel.class, "userPlanScaleRadioButton.mnemonic"))
					.getKeyCode());
			this.headerFormatLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PageSetupPanel.class, "headerFormatLabel.mnemonic"))
					.getKeyCode());
			this.headerFormatLabel.setLabelFor(this.headerFormatTextField);
			this.footerFormatLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PageSetupPanel.class, "footerFormatLabel.mnemonic"))
					.getKeyCode());
			this.footerFormatLabel.setLabelFor(this.footerFormatTextField);
		}
	}
	
	/**
	 * Layouts panel components in panel with their labels. 
	 */
	private void layoutComponents()
	{
		int labelAlignment = OperatingSystem.isMacOSX() ? GridBagConstraints.LINE_END : GridBagConstraints.LINE_START;
		// First row
		JPanel topPanel = new JPanel(new GridBagLayout());
		topPanel.add(this.pageFormatButton, new GridBagConstraints(0, 0, 2, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0));
		Insets lastComponentInsets = new Insets(0, 0, 5, 0);
		// Furniture component
		topPanel.add(this.furniturePrintedCheckBox, new GridBagConstraints(0, 1, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, lastComponentInsets, 0, 0));
		// Plan components
		topPanel.add(this.planPrintedCheckBox, new GridBagConstraints(0, 2, 2, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 2, 0), 0, 0));
		topPanel.add(this.bestFitPlanScaleRadioButton, new GridBagConstraints(0, 3, 2, 1, 0, 0,
				GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 20, 2, 0), 0, 0));
		topPanel.add(this.userPlanScaleRadioButton, new GridBagConstraints(0, 4, 1, 1, 0, 0, labelAlignment,
				GridBagConstraints.NONE, new Insets(0, 20, 5, 0), 0, 0));
		topPanel.add(this.userPlanScaleSpinner, new GridBagConstraints(1, 4, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, lastComponentInsets, 0, 0));
		// 3D view component
		topPanel.add(this.view3DPrintedCheckBox, new GridBagConstraints(0, 5, 2, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		
		add(topPanel, new GridBagConstraints(0, 0, 2, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
				new Insets(0, 0, 5, 0), 0, 0));
		// Second row
		add(new JSeparator(), new GridBagConstraints(0, 1, 2, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), -10, 0));
		// Third row
		add(this.headerFormatLabel, new GridBagConstraints(0, 2, 1, 1, 0, 0, labelAlignment, GridBagConstraints.NONE,
				new Insets(0, 0, 5, 5), 0, 0));
		add(this.headerFormatTextField, new GridBagConstraints(1, 2, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, lastComponentInsets, 0, 0));
		// Forth row
		add(this.footerFormatLabel, new GridBagConstraints(0, 3, 1, 1, 0, 0, labelAlignment, GridBagConstraints.NONE,
				new Insets(0, 0, 5, 5), 0, 0));
		add(this.footerFormatTextField, new GridBagConstraints(1, 3, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, lastComponentInsets, 0, 0));
		// Last row
		add(this.variablesLabel, new GridBagConstraints(0, 4, 1, 1, 0, 0, labelAlignment, GridBagConstraints.NONE,
				new Insets(0, 0, 0, 5), 0, 0));
		add(this.variableButtonsToolBar, new GridBagConstraints(1, 4, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
	}
	
	/**
	 * Displays this panel in a modal dialog box. 
	 */
	public void displayView(View parentView)
	{
		if (SwingTools.showConfirmDialog((JComponent) parentView, this, this.dialogTitle,
				this.pageFormatButton) == JOptionPane.OK_OPTION && this.controller != null)
		{
			this.controller.modifyPageSetup();
		}
	}
}
